package fr.efrei.paumier.common.time;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

public abstract class BaseGameEngineTests {

	private List<Event> eventTriggered = new ArrayList<Event>();
	
	protected abstract GameEngineAndQueue createGameEngine(Clock clock);

	@Test
	public void update_lowerThanAnyTime_nothingHappens() {
		FakeClock clock = new FakeClock(Clock.fixed(Instant.EPOCH, ZoneId.systemDefault()));		
		GameEngineAndQueue manager = createGameEngine(clock);

		Event event = createEvent(Duration.ofSeconds(10));		
		manager.register(event);

		clock.advance(Duration.ofSeconds(1));
		manager.update();
		
		assertEquals(0, eventTriggered.size());
	}

	@Test
	public void update_higherThanOnlyEvent_eventHappens() {
		FakeClock clock = new FakeClock(Clock.fixed(Instant.EPOCH, ZoneId.systemDefault()));		
		GameEngineAndQueue manager = createGameEngine(clock);
		
		Event event = createEvent(Duration.ofSeconds(10));		
		manager.register(event);		

		clock.advance(Duration.ofSeconds(15));
		manager.update();

		assertEquals(1, eventTriggered.size());
		assertSame(event, eventTriggered.get(0));
	}

	@Test
	public void update_multipleSmallTimes_eventHappens() {
		FakeClock clock = new FakeClock(Clock.fixed(Instant.EPOCH, ZoneId.systemDefault()));		
		GameEngineAndQueue manager = createGameEngine(clock);
		
		FakeEvent event = createEvent(Duration.ofSeconds(9), manager);		
		manager.register(event);		

		clock.advance(Duration.ofSeconds(2));
		manager.update();
		
		clock.advance(Duration.ofSeconds(2));
		manager.update();
		
		clock.advance(Duration.ofSeconds(2));
		manager.update();
		
		clock.advance(Duration.ofSeconds(2));
		manager.update();
		
		clock.advance(Duration.ofSeconds(2));
		manager.update();
		
		clock.advance(Duration.ofSeconds(2));
		manager.update();

		assertEquals(1, eventTriggered.size());
		assertSame(event, eventTriggered.get(0));
		assertEquals(Instant.EPOCH.plusSeconds(9), event.getTriggeredInstant());
	}
	
	@Test
	public void update_equalsToOnlyEvent_eventHappens() {
		FakeClock clock = new FakeClock(Clock.fixed(Instant.EPOCH, ZoneId.systemDefault()));		
		GameEngineAndQueue manager = createGameEngine(clock);
		
		Event event = createEvent(Duration.ofSeconds(10));			
		manager.register(event);		

		clock.advance(Duration.ofSeconds(10));
		manager.update();

		assertEquals(1, eventTriggered.size());
		assertSame(event, eventTriggered.get(0));
	}

	@Test
	public void update_betweenTwoEvents_firstEventHappens() {
		FakeClock clock = new FakeClock(Clock.fixed(Instant.EPOCH, ZoneId.systemDefault()));		
		GameEngineAndQueue manager = createGameEngine(clock);

		Event event1 = createEvent(Duration.ofSeconds(20));
		Event event2 = createEvent(Duration.ofSeconds(10));		
		manager.register(event1, event2);		

		clock.advance(Duration.ofSeconds(15));
		manager.update();

		assertEquals(1, eventTriggered.size());
		assertSame(event2, eventTriggered.get(0));
	}

	@Test
	public void update_AfterTwoEvents_firstEventHappensBeforeOther() {
		FakeClock clock = new FakeClock(Clock.fixed(Instant.EPOCH, ZoneId.systemDefault()));		
		GameEngineAndQueue manager = createGameEngine(clock);

		Event event1 = createEvent(Duration.ofSeconds(20));
		Event event2 = createEvent(Duration.ofSeconds(10));		
		manager.register(event1, event2);		

		clock.advance(Duration.ofSeconds(30));
		manager.update();

		assertEquals(2, eventTriggered.size());
		assertSame(event2, eventTriggered.get(0));
		assertSame(event1, eventTriggered.get(1));
	}

	@Test
	public void update_eventRateChanges_updateTimeWithProrateRespect() {
		FakeClock clock = new FakeClock(Clock.fixed(Instant.EPOCH, ZoneId.systemDefault()));		
		GameEngineAndQueue manager = createGameEngine(clock);

		FakeEvent event = createEvent(Duration.ofSeconds(10));
		
		manager.register(event);		
		assertEquals(0, eventTriggered.size());
		
		manager.update();
		assertEquals(0, eventTriggered.size());
		
		clock.advance(Duration.ofSeconds(3));
		manager.update();
		assertEquals(0, eventTriggered.size());
		
		clock.advance(Duration.ofSeconds(3));
		manager.update();
		event.setRate(2);
		assertEquals(0, eventTriggered.size());
		
		clock.advance(Duration.ofSeconds(1));
		manager.update();
		assertEquals(0, eventTriggered.size());
		
		clock.advance(Duration.ofSeconds(1));
		manager.update();
		
		assertEquals(1, eventTriggered.size());
		assertSame(event, eventTriggered.get(0));
	}

	@Test
	public void update_rateChanges_andNewEvents_andMultipleSteps_reorderEvents() {
		FakeClock clock = new FakeClock(Clock.fixed(Instant.EPOCH, ZoneId.systemDefault()));		
		GameEngineAndQueue manager = createGameEngine(clock);

		FakeEvent event1 = createEvent(Duration.ofSeconds(8), manager);
		FakeEvent event2 = createEvent(Duration.ofSeconds(30), manager);
		FakeEvent event3 = createEvent(Duration.ofSeconds(2), manager);
		
		manager.register(event1, event2);		
		manager.update();
		
		clock.advance(Duration.ofSeconds(3));
		manager.update();
		event2.setRate(9);
		manager.register(event3);
		
		for (int i = 3; i < 8; i++) {
			clock.advance(Duration.ofSeconds(1));
			manager.update();
		}
		
		assertEquals(3, eventTriggered.size());
		assertSame(event3, eventTriggered.get(0));
		assertSame(event2, eventTriggered.get(1));
		assertSame(event1, eventTriggered.get(2));

		assertEquals(Instant.EPOCH.plusSeconds(5), event3.getTriggeredInstant());
		assertEquals(Instant.EPOCH.plusSeconds(6), event2.getTriggeredInstant());
		assertEquals(Instant.EPOCH.plusSeconds(8), event1.getTriggeredInstant());
	}

	@Test
	public void update_rateChanges_andNewEvents_andBigStep_reorderEvents() {
		FakeClock clock = new FakeClock(Clock.fixed(Instant.EPOCH, ZoneId.systemDefault()));		
		GameEngineAndQueue manager = createGameEngine(clock);

		FakeEvent event1 = createEvent(Duration.ofSeconds(8), manager);
		FakeEvent event2 = createEvent(Duration.ofSeconds(30), manager);
		FakeEvent event3 = createEvent(Duration.ofSeconds(2), manager);
		
		manager.register(event1, event2);		
		manager.update();
		
		clock.advance(Duration.ofSeconds(3));
		manager.update();
		event2.setRate(9);
		manager.register(event3);
		
		clock.advance(Duration.ofSeconds(10));
		manager.update();
		
		assertEquals(3, eventTriggered.size());
		assertSame(event3, eventTriggered.get(0));
		assertSame(event2, eventTriggered.get(1));
		assertSame(event1, eventTriggered.get(2));

		assertEquals(Instant.EPOCH.plusSeconds(5), event3.getTriggeredInstant());
		assertEquals(Instant.EPOCH.plusSeconds(6), event2.getTriggeredInstant());
		assertEquals(Instant.EPOCH.plusSeconds(8), event1.getTriggeredInstant());
		assertEquals(Instant.EPOCH.plusSeconds(13), manager.getCurrentInstant());
	}

	private FakeEvent createEvent(Duration duration) {
		return new FakeEvent(Instant.EPOCH, duration, null, this.eventTriggered);
	}

	private FakeEvent createEvent(Duration duration, GameEngine manager) {
		return new FakeEvent(Instant.EPOCH, duration, manager, this.eventTriggered);
	}
}